DIY USB 电流表(7):读取和显示 INA219 电流电压数据
在前一篇 《DIY USB 电流表(6):点个屏,使用 I2C 驱动 0.96 寸 OLED》 中,我们已经完成了屏幕显示驱动的开发,并且根据需求,列出了需要展示的数据项,确定了一下最终显示内容的布局。
在之前,显示的内容都是占位的测试数据,在这一节,就可以开始真正去读取 INA219 传感器的数据,将电路中测量的电压、电流等数据显示在屏幕上,这又是一节枯燥的编码工作 🙈。
PS. 我也还是一个初学者,如果文章中有一些错误或不足,还请多多指教。
准备工作
在开始读取 INA219 的数据之前,同样也要准备一些相关的库,例如 INA219 数据的读取、参数的配置,以及在 CH32V003-GameConsole 中所使用的 I2C 封装并未提供读取 I2C 数据方法,另外,为了计算 USB 电流表运行时间内消耗的电量,也需要有相关的计时方法。
I2C 读取方法
在 CH32V003-GameConsole 的代码中,已经有了 i2c_tx.h
,提供了 I2C 写入相关的方法,对于一个游戏机来说,拥有写入方法就足够了,它只需要用 I2C 来刷新屏幕,但是在我们的 USB 电流表项目中,还需要使用 I2C 去读取 INA219 的数据,因此需要去封装一个 I2C 读取方法。
在开始编写 I2C 读取方法之前,先将 i2c_tx.c
文件中初始化 I2C 控制器的代码修改一下,启用自动发送 ACK 的标志位,防止后续读取失败。
配置完后,在 i2c_tx.h
中添加 I2C 读取相关方法的定义:
// 开始读取 I2C 数据
void I2C_start_read(uint8_t addr);
// 从 I2C 总线读取 1 个字节
uint8_t I2C_read();
// 停止读取 I2C 数据
void I2C_read_stop(void);
接下来完成相关 I2C 读取方法的编写,在 i2c_tx.c
文件的末尾添加以下代码:
// 根据地址设置写入标志位
#define OADDR1_ADD0_Set ((uint8_t)0x01)
// I2C 控制器读取状态
#define I2C_RECV_MODE_SELECTED ((uint32_t)0x00020003) /* BUSY, MSL and ADDR flags */
void I2C_start_read(uint8_t addr) {
addr |= OADDR1_ADD0_Set;
while(I2C1->STAR2 & I2C_STAR2_BUSY); // wait until bus ready
I2C1->CTLR1 |= I2C_CTLR1_START; // set START condition
while(!(I2C1->STAR1 & I2C_STAR1_SB)); // wait for START generated
I2C1->DATAR = addr; // send slave address + R/W bit
while(!I2C_checkEvent(I2C_RECV_MODE_SELECTED)); // wait for address transmitted
}
uint8_t I2C_read() {
while (!(I2C1->STAR1 & I2C_STAR1_RXNE));
return I2C1->DATAR;
}
void I2C_read_stop(void) {
while(!(I2C1->STAR1 & I2C_STAR1_RXNE)); // wait for last byte transmitted
I2C1->CTLR1 |= I2C_CTLR1_STOP; // set STOP condition
}
完成这些之后,就可以开始适配一下 INA219 的驱动到 CH32V003 上了。
INA219 驱动适配
INA219 的驱动,我使用了一个之前在 ESP32 上使用的库 INA219_WE,将它稍微变化一下,适配到 CH32V003 上来使用。
项目地址:https://github.com/wollewald/INA219_WE
将项目代码下载下来之后,我们只需要使用 INA219_WE.h
和 INA219_WE.cpp
两个文件,将它们复制到 USB 电流表固件项目的 src/drivers
目录中。
接下来的适配工作,主要变更的有以下几个地方:
去除 Arduino 相关的头文件
构造方法仅保留一个传入 I2C 地址的方法
补全缺失的 Arduino 中相关方法
将
writeRegister
和readRegister
适配为i2c_tx.c
中的相关方法
前面两点比较好修改,在 INA219_WE.h
中删除对应的代码就可以,就不多赘述,接下来的修改都在 INA219_WE.cpp
中完成。
添加头文件和缺失定义
因为默认情况下,项目中没有 byte 类型、abs、delayMicroseconds 这三个的定义,因此需要提供定义一下。
#include <debug.h>
#include "i2c_tx.h"
#define byte uint8_t
#define abs(v) (v > 0 ? v : -v)
#define delayMicroseconds(ms) Delay_Ms(ms)
适配 wrigeRegister 和 readRegister
将原代码中的这两个方法替换成以下内容,主要是将 Wire 相关的调用,替换成 i2c_tx.h
中相关方法的调用:
uint8_t INA219_WE::writeRegister(uint8_t reg, uint16_t val){
I2C_start(i2cAddress);
uint8_t lVal = val & 255;
uint8_t hVal = val >> 8;
I2C_write(reg);
I2C_write(hVal);
I2C_write(lVal);
I2C_stop();
return 0;
}
uint16_t INA219_WE::readRegister(uint8_t reg){
uint8_t MSByte = 0, LSByte = 0;
uint16_t regValue = 0;
I2C_start(i2cAddress);
I2C_write(reg);
I2C_stop();
I2C_start_read(i2cAddress);
MSByte = I2C_read();
LSByte = I2C_read();
I2C_read_stop();
regValue = (MSByte<<8) + LSByte;
return regValue;
}
这样就完成了 INA219 驱动的适配,可以直接通过 INA219_WE 的类来读取 INA219 传感器数据了。
获取系统运行时间
为了计算 USB 电流表统计的电量,除了功率之后,还需要运行对应功率的时间,在默认情况下,CH32V003 SDK 并未提供类似 Arduino 中 millis()
这样的方法来获取系统运行时间,因此我们需要额外编写一个方法用来获取运行时间。
这个可以从 arduino_core_ch32
项目中获取实现,基于系统的 Tick 中断计数来实现运行时间的统计。
代码地址:https://github.com/Community-PIO-CH32V/arduino_core_ch32/blob/main/cores/arduino/ch32/clock.c
void systick_init(void)
{
SysTick->SR = 0;
SysTick->CTLR= 0;
SysTick->CNT = 0;
SysTick->CMP = SystemCoreClock / 1000 - 1;
SysTick->CTLR= 0xF;
NVIC_SetPriority(SysTicK_IRQn,0xFF);
NVIC_EnableIRQ(SysTicK_IRQn);
}
有了获取运行时间的方法之后,就可以计算累计消耗电量了。
读取 INA219 数据并显示
万事俱备,只欠东风,准备好所有驱动和相关依赖之后,就可以读取真实 INA219 的传感器数据了。
我们将 INA219 数据读取、计算相关的代码,都放一个独立的文件中来管理,就叫 data_manager.hpp
吧。
定义数据存储变量
在这里定义了采集电阻大小为 10mΩ,另外可以看到这里定义了 INA219 的 I2C 为 0x40 <<
,这是因为在从 CH32V003-GameConsole 拿来使用的 I2C 驱动,是直接将读写位交给外部控制了,因此这里需要提前左移一位。
#include "drivers/INA219_WE.h"
#include <string.h>
#include "drivers/clock.h"
#define INA219_I2C_ADDRESS (0x40 << 1)
#define SHUNT_SIZE_IN_MR 10
INA219_WE ina219(INA219_I2C_ADDRESS);
int32_t measure_vbus_mv = 0;
int32_t measure_vshunt_uv = 0;
int32_t measure_current_ma = 0;
int32_t measure_power_mw = 0;
int32_t measure_cap_mwh = 0;
int32_t measure_cap_last_time = 0;
初始化 INA219 驱动
作为一个电流表,我们需要持续监测整个电路和功率,因此在这里将 INA219 初始化为持续采样模式。
void adc_setup() {
ina219.init();
ina219.setMeasureMode(CONTINUOUS);
ina219.setPGain(PG_80);
ina219.setShuntSizeInOhms(SHUNT_SIZE_IN_MR / 1000);
}
另外这里将 INA219 的增益模式设置为 PG_80,根据数据手册,表示测量压差范围为 80mV:
这里使用的采样电阻大小为 10mΩ,在 PD 2.0 100W 的情况下,最大电流为 5A,通过计算 5A * 10mΩ = 50mV
可以得出最大压降为 50mV,因此设置测量范围为 80mV 完全够用。
读取数据并计算功率、电量
这里采用积分的方式来计算累计的电量,将当前计算出来的功率视为从上一次计算到当前时间点的持续功率,从而计算出这段时间的电量,再每次进行累计。
void adc_read_values() {
measure_vbus_mv = ina219.getBusVoltage_V() * 1000;
measure_vshunt_uv = ina219.getShuntVoltage_mV() * 1000;
if (measure_vshunt_uv < 0) {
measure_vshunt_uv = 0;
}
measure_current_ma = measure_vshunt_uv / SHUNT_SIZE_IN_MR;
measure_power_mw = measure_vbus_mv * measure_current_ma / 1000;
uint32_t now = millis();
uint32_t period = now - measure_cap_last_time;
measure_cap_last_time = now;
int32_t period_cap = measure_power_mw * period / 1000;
measure_cap_mwh += period_cap;
}
显示数据
在这里先写一个 show_power_metrics
方法将刷新屏幕,显示数据相关的操作封装起来。
这里显示数据就比较简单,将电压、电流、功率、电量四个数字格式化到缓冲区中,并通过显示驱动显示到每一行上。
void show_power_metrics() {
char buf[17];
display_clear();
snprintf(buf, 17, (const char *)"Volt : %dmV", (int)measure_vbus_mv);
display_write_line(0, buf);
snprintf(buf, 17, (const char *)"Curr : %dmA", (int)measure_current_ma);
display_write_line(1, buf);
snprintf(buf, 17, (const char *)"Power: %dmW", (int)measure_power_mw);
display_write_line(2, buf);
snprintf(buf, 17, (const char *)"Cap : %dmWh", (int)(measure_cap_mwh / 3600));
display_write_line(3, buf);
display_flush();
}
持续更新数据
最终,我们修改一下 main
方法,在死循环中持续读取 INA219 并更新到屏幕上,就完成了 USB 电流表的核心功能。
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
SystemCoreClockUpdate();
Delay_Init();
systick_init();
// 初始化屏幕驱动
display_init();
// 初始化 INA219 驱动
adc_setup();
while (1) {
// 读取 INA219
adc_read_values();
// 刷新屏幕显示
show_power_metrics();
}
}
实际运行效果
在这里,将固件烧录进 USB 电流表,并且在输出端接上负载,就可以在屏幕上看到当前负载消耗的实时功率了。
这里使用的电源输入是 5V,可以看到 USB 电流表检测到的电压只有 4.8V 左右,这是因为通常 USB 电缆都有内阻,在有电流时,会有压降,最终到达负载时的电压就跟电源端的电压不一致了。
一般来说,更好的 USB 线材会拥有更低的内阻,在大功率充电的场景中,也会拥有发热更低的优势。
小结
至此,我们已经完成了 DIY USB 电流表的核心功能,显示电压、电流等数据,如果不需要额外的功能,在这个步骤已经可以算是完工了 😃。
当然在硬件上我们已经预留了两个按钮,还是可以做一些其他功能进来,例如显示功率曲线?这样就可以在给设备充电过程中观察到充电功率的变化了。
那么,下一篇来完成记录功率历史和绘制功率曲线。
USB 电流表开源地址
这个 USB 电流表所有资料已经开源,可以在以下仓库中获取,包含固件代码、PCB 生产 Gerber 文件、原理图和外壳 STL 文件。
https://github.com/ohdarling/CH32V003-USBMeter
硬件相关的源文件已经在立创开源平台开源,访问以下地址可以进行一键 PCB 下单和一键 BOM 配单操作:
https://oshwhub.com/wandaeda/ji-yu-ch32v003-de-usb-dian-liu-biao
DIY USB 电流表系列
其他 DIY 项目
30 元 DIY 一个柔性灯丝氛围灯
教程地址: https://xujiwei.com/blog/2024/04/diy-ambient-light/
参考资料
https://github.com/wagiminator/CH32V003-GameConsole
https://github.com/wollewald/INA219_WE
https://github.com/ohdarling/CH32V003-USBMeter
https://oshwhub.com/wandaeda/ji-yu-ch32v003-de-usb-dian-liu-biao
https://github.com/Community-PIO-CH32V/arduino_core_ch32
https://www.ti.com/product/INA219
https://www.ti.com/lit/ds/symlink/ina219.pdf